Глава 1. Общая информация о подходах к работе с памятью
В первых языках программирования (до первых версий алгола) сущестовал только статический способ выделения памяти, в таком случае память выделалась на этапе компиляции. В таком случае необходимо было перекомпилировать программу если памяти было выделено недостаточно. На данный момент такой способ выделения памяти применяется в глобальных и статических переменных.
На данный момент все физически реализации компьютеров - регистровые машины, в них существуют регистры и разны виды памяти, но так было не всегда. Раньше существовали стековые машины, где вся память была только стеком и у неё были преимущества.
У данной машины нет вопроса где хранить переменные.
У данной машины меньше размеры команд, т.к. большинство команд такого ассемблера будут без параметров (например достать последние два числа стека и сложить).
В итоге реализация физического стека оказалась слишком сложной, но! к этой идее вернулись при реализации JVM и .Net. Эти среды выполнения являются стековыми машинами.
Каждому потоку выделяется свой стек но на всех одна куча и одна статическая память
Сборка мусора
Введем следующие абстракции
Модификатор, это абстракция над программой которая изменяет состояние памяти можно расширить даже до чтения. Пример реализации такой абстракции это поток.
Аллокатор, это абстракция над программой которая отвечает на выделение и выдачу памяти
Сборщик, абстракция над программой которая находит объекты которые не будут использованы в будущем и чистит их.
Подходы к сборке
Существет подход к сборке мусора в виде подсчета ссылок, такой подход например реализован в c++. Он быстрый, может быть реализован на уровне языка, и высвобождает память сразу как только память не используется, однако есть проблемы с циклическими ссылками, а также с многопоточностью.
Существуют нулевые сборщики мусора, они нужны для программ которые работают очень мало и для них допустим неконтролируемый рост используемой памяти. (такие сборщики вообще ничего не делают)
Существует консервативная реализация GC. Данный подход подразумевает, что модификатор не способен дать информацию о переменных. В таком случае GC бродит по регистрам и по стеку и ищет что то похожее на указатели, если находит то не чистит этот участок. Плюс что данный подход можно реализовать на уровне библиотек, поэтому такой GC есть даже на плюсы. (до 2010г такое применялось в Mono). Минус что в таком случае нельзя производить уплотнение т.к. ничего в регистрах не поменяешь, а еще долго и не точно.
Существует алгоритм точной сборки мусора. В данном подходе среда выполнения предоставляет самостоятельно информацию о текущих корнях и объектах, а сборщику остается только пройтись по ссылкам и поискать неактивные объекты.
Этап сборки включает процесс очистки.
Реализации процесса очистки:
Наивная реализация подразумевает просто выставления бита объекта отвечающего за то что он больше не используется в true. Также данный подход подразумевает существование некой структуры которая бы сохраняла информацию о том, какие участки памяти можно занять. Данный метод прост в реализации, но приводит к постепенной и неминуемой сильной фрагментации.
Реализация с уплотнением копированием. Данный подход использует JVM. Данная реализация подразумевает существование двух участков памяти одного размера. Когда сборщик проходит по памяти и находит используемые объекты, он их перекопирует в другой участок памяти с уплотнением. Таким образом мы не храним информацию о пропусках в памяти и избегаем фрагментации, но вынуждены удвоить память и постоянно копировать.
буква T это мусор
1. |__________|ABCTDT____|
2. |ABCD______|__________|
Реализация с уплотнением на месте. Данный подход сложнее в реализации и подразумевает копирование объектов только необходимых с целью хранить их компактнее. Данный подход используется в .Net.
буква Т это мусор
1. |ABCTDT_________|
2. |ABCD___________|